/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah <amrullah@panemu.com>.
 */
package com.abc.cheque.control.skin;


import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;

import com.abc.cheque.control.DateField;
import com.abc.cheque.control.behavior.DateFieldBehavior;
import com.abc.cheque.control.calendar.CalendarView;
import com.abc.cheque.ui.form.BaseControl;
import com.sun.javafx.scene.control.FocusableTextField;
import com.sun.javafx.scene.control.skin.SkinBase;

/**
 * Based on Christian Schudt's DatePicker
 * http://myjavafx.blogspot.com/2012/01/javafx-calendar-control.html
 *
 * @author Amrullah <amrullah@panemu.com>
 */
public class DateFieldSkin extends SkinBase<DateField, DateFieldBehavior> {

    private TextField textField;
    private Button button;
    private Popup popup;
    private CalendarView calendarView;
    private static final String PROP_SHOWING_CALENDAR = "SHOWING_CALENDAR";
    private BooleanProperty invalid = new SimpleBooleanProperty();
    private DateField dateField;

    public DateFieldSkin(DateField control) {
        super(control, new DateFieldBehavior(control));
        this.dateField = control;
        initCalendarView();
        // move focus in to the textfield if the comboBox is editable
        dateField.focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
                if (!t1) {
                    tryParse(false);
                }
                updateFakeFocus(dateField.isFocused());
            }
        });
        initialize();
        dateField.addEventFilter(InputEvent.ANY, new EventHandler<InputEvent>() {
            @Override public void handle(InputEvent t) {
                if (textField == null) return;
                
                // When the user hits the enter or F4 keys, we respond before 
                // ever giving the event to the TextField.
                if (t instanceof KeyEvent) {
                    KeyEvent ke = (KeyEvent)t;
                    
                    if (ke.getCode() == KeyCode.F10 || ke.getCode() == KeyCode.ESCAPE || ke.getCode() == KeyCode.ENTER) {
                        // RT-23275: The TextField fires F10 and ESCAPE key events
                        // up to the parent, which are then fired back at the 
                        // TextField, and this ends up in an infinite loop until
                        // the stack overflows. So, here we consume these two
                        // events and stop them from going any further.
                        t.consume();
                        return;
                    }
                }
                
                textField.fireEvent(t);
            }
        });
        
        requestLayout();
        registerChangeListener(control.showingCalendarProperty(), PROP_SHOWING_CALENDAR);
    };

    private void initialize() {
        textField = new FocusableTextField();
        textField.setFocusTraversable(true);
        textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) {
                if (hasFocus) {
                    // Using Platform.runLater here, as without it it seems we
                    // enter into some form of race condition where we are 
                    // wanting to set focus on the comboBox whilst the textField
                    // is still notifying of its focus gain.
                    // This issue was identified in RT-21088.
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            dateField.requestFocus();
                        }
                    });
                }

                updateFakeFocus(hasFocus);

                // RT-21454 starts here
//                if (! hasFocus) {
//                    setTextFromTextFieldIntoComboBoxValue();
//                }
            }
        });
        // Let the prompt text property listen to locale or date format changes.
        textField.promptTextProperty().bind(new StringBinding() {
            {
                super.bind(getSkinnable().localeProperty(), getSkinnable().promptTextProperty(), getSkinnable().dateFormatProperty());
            }

            @Override
            protected String computeValue() {
                // First check, if there is a custom prompt text.
                if (getSkinnable().promptTextProperty().get() != null) {
                    return getSkinnable().promptTextProperty().get();
                }

                // If not, use the the date format's pattern.
                DateFormat dateFormat = getActualDateFormat();
                if (dateFormat instanceof SimpleDateFormat) {
                    return ((SimpleDateFormat) dateFormat).toPattern();
                }

                return "";
            }
        });
        
        dateField.markInvalidProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (oldValue && !newValue && invalid.get()) {
                    tryParse(false);
                }
            }
        });

        
        // Listen to user input.
        textField.textProperty().addListener(new ChangeListener<String>() {
            @Override
            public void changed(ObservableValue<? extends String> observableValue, String s, String s1) {
                // Only evaluate the input, it it wasn't set programmatically.
                if (textSetProgrammatically) {
                    return;
                }
                dateField.markInvalidProperty().set(true);
                invalid.set(true);
                // If the user clears the text field, set the date to null and the field to valid.
                if (s1.equals("")) {
                    dateField.selectedDateProperty().set(null);
                    invalid.set(false);
                }
            }
        });

        if (dateField.selectedDateProperty().get() != null) {
            updateTextField();
        }
        dateField.selectedDateProperty().addListener(new ChangeListener<Date>() {
            @Override
            public void changed(ObservableValue<? extends Date> ov, Date t, Date t1) {
                updateTextField();
                invalid.set(false);
            }
        });

        dateField.localeProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                updateTextField();
            }
        });

        button = new Button();
        button.setFocusTraversable(false);
        button.setGraphic(new ImageView(new Image(BaseControl.class.getResourceAsStream("/images/calendar.png"))));
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent t) {
                if (!dateField.isFocused()) {
                    /**
                     * Need to make this control become focused. Otherwise
                     * changing value in DateColumn while the DateField cell editor
                     * is not focused before, won't trigger commitEdit()
                     */
                    dateField.requestFocus();
                }
                dateField.showCalendar();
            }
        });
        StackPane.setAlignment(textField, Pos.CENTER_LEFT);
        StackPane.setAlignment(button, Pos.CENTER_RIGHT);
        this.getChildren().addAll(textField, button);
    }
    
    private void updateFakeFocus(boolean b) {
        if (textField == null) {
            return;
        }
        if (!(textField instanceof FocusableTextField)) {
            return;
        }
        ((FocusableTextField) textField).setFakeFocus(b);
    }

    @Override
    protected void handleControlPropertyChanged(String string) {
        super.handleControlPropertyChanged(string);
        if (PROP_SHOWING_CALENDAR.equals(string)) {
            if (getSkinnable().isShowing()) {
                showPopup();
            } else {
                if (popup != null) {
                    popup.hide();
                }
            }
        }
    }

    /**
     * Tries to parse the text field for a valid date.
     *
     * @param setDateToNullOnException True, if the date should be set to null,
     * when a {@link ParseException} occurs. This is the case, when the text
     * field loses focus.
     */
    private void tryParse(boolean setDateToNullOnException) {
        try {
            // Double parse the date here, since e.g. 01.01.1 is parsed as year 1, and then formatted as 01.01.01 and then parsed as year 2001.
            // This might lead to an undesired date.
            DateFormat dateFormat = getActualDateFormat();
            Date parsedDate = dateFormat.parse(textField.getText());
            parsedDate = dateFormat.parse(dateFormat.format(parsedDate));
            if (getSkinnable().selectedDateProperty().get() == null || getSkinnable().selectedDateProperty().get() != null && parsedDate.getTime() != getSkinnable().selectedDateProperty().get().getTime()) {
                getSkinnable().selectedDateProperty().set(parsedDate);
            }
            invalid.set(false);
            updateTextField();
        } catch (ParseException e) {
            invalid.set(true);
            if (setDateToNullOnException) {
                getSkinnable().selectedDateProperty().set(null);
            }
        }

    }

    /**
     * Gets the actual date format. If {@link #dateFormatProperty()} is set,
     * take it, otherwise get a default format for the current locale.
     *
     * @return The date format.
     */
    private DateFormat getActualDateFormat() {
        if (getSkinnable().dateFormatProperty().get() != null) {
            return getSkinnable().dateFormatProperty().get();
        }

        DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, getSkinnable().localeProperty().get());
        format.setCalendar(calendarView.getCalendar());
        format.setLenient(false);

        return format;
    }
    private boolean textSetProgrammatically;

    /**
     * Updates the text field.
     */
    private void updateTextField() {
        // Mark the we update the text field (and not the user), so that it can be ignored, by textField.textProperty()
        textSetProgrammatically = true;
        if (getSkinnable().selectedDateProperty().get() != null) {
            String date = getActualDateFormat().format(getSkinnable().selectedDateProperty().get());
            if (!textField.getText().equals(date)) {
                textField.setText(date);
            }
        } else {
            textField.setText("");
        }
        textSetProgrammatically = false;
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();
        double obw = button.prefWidth(-1);
        
        double displayWidth = getSkinnable().getWidth()
                - (getInsets().getLeft() + getInsets().getRight() + obw);

        Insets padding = getInsets();
        double x = padding.getLeft();
        double y = padding.getTop();
        final double w = getSkinnable().getWidth() - (padding.getLeft() + padding.getRight());
        final double h = getSkinnable().getHeight() - (padding.getTop() + padding.getBottom());
        textField.resizeRelocate(x, y, w, h);
        button.resizeRelocate(x + displayWidth, y, obw, h);
    }

    private void initCalendarView() {
        calendarView = new CalendarView(getSkinnable().localeProperty().get());

        calendarView.setEffect(new DropShadow());

        // Use the same locale.
        calendarView.localeProperty().bind(getSkinnable().localeProperty());

        // Bind the current date of the calendar view with the selected date, so that the calendar shows up with the same month as in the text field.
        calendarView.currentDateProperty().bind(getSkinnable().selectedDateProperty());

        // When the user selects a date in the calendar view, hide it.
        calendarView.selectedDateProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                dateField.selectedDateProperty().set(calendarView.selectedDateProperty().get());
//                hidePopup();
                getSkinnable().hideCalendar();
            }
        });
        
        calendarView.showTodayButtonProperty().bind(getSkinnable().showTodayButtonProperty());
    }

    private void showPopup() {

        if (popup == null) {
            popup = new Popup();
            popup.setAutoHide(true);
            popup.setHideOnEscape(true);
            popup.setAutoFix(true);
            popup.getContent().add(calendarView);
            popup.setOnAutoHide(new EventHandler<Event>() {
                @Override
                public void handle(Event e) {
                    getSkinnable().hideCalendar();
                }
            });
        }

        Bounds calendarBounds = calendarView.getBoundsInLocal();
        Bounds bounds = localToScene(getBoundsInLocal());

        double posX = calendarBounds.getMinX() + bounds.getMinX() + getScene().getX() + getScene().getWindow().getX();
        double posY = calendarBounds.getMinY() + bounds.getHeight() + bounds.getMinY() + getScene().getY() + getScene().getWindow().getY();

        popup.show(this, posX, posY);

    }
};
